package com.tianji.learning.service.impl;

import com.tianji.common.autoconfigure.mq.RabbitMqHelper;
import com.tianji.common.constants.MqConstants;
import com.tianji.common.exceptions.BizIllegalException;
import com.tianji.common.utils.BooleanUtils;
import com.tianji.common.utils.CollUtils;
import com.tianji.common.utils.DateUtils;
import com.tianji.common.utils.UserContext;
import com.tianji.learning.constants.RedisConstants;
import com.tianji.learning.domain.vo.SignResultVO;
import com.tianji.learning.mq.message.SignInMessage;
import com.tianji.learning.service.ISignRecordService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.util.List;

//签到相关接口实现类
@Service
@RequiredArgsConstructor
@Slf4j
public class SignRecordServiceImpl implements ISignRecordService {

    private final StringRedisTemplate redisTemplate;

    private final RabbitMqHelper rabbitMqHelper;

    /**
     * 签到功能接口
     *
     * @return SignResultVO 签到结果
     */
    @Override
    public SignResultVO addSignRecords() {
        log.info("签到功能执行");
        //1.获取当前登录用户id；
        Long userId = UserContext.getUser();

        //2.获取当天日期；
        LocalDate now = LocalDate.now();

        //3.组装BitMap的key：前缀：sign:uid:用户id:月份信息
        String key = RedisConstants.SIGN_RECORD_KEY_PREFIX
                + userId
                + now.format(DateUtils.SIGN_DATE_SUFFIX_FORMATTER);  //:20230730

        //4.计算offset，要存储在容器中的索引位置。对应这个月的哪一日；
        //如果是1日，对应的就是0索引上的二进制；2日就是1
        int offset = now.getDayOfMonth() - 1;

        //5.写入Redis，使用BitMap结构，保存用户的签到信息；
        //返回值：被覆盖的旧二进制数字；
        Boolean exists = redisTemplate.opsForValue().setBit(key, offset, true); //true=1
        //判断今日是否已经签到，0表示未签到
        if (BooleanUtils.isTrue(exists)) {
            //为true=1，表示当前位置的旧数据是1，已签到过
            throw new BizIllegalException("不允许重复签到！");
        }


        //6.计算当前用户本月的连续签到天数；
        int signDays = countSignDays(key, now.getDayOfMonth());


        //8.计算连续签到得分；
        int rewardPoints = 0;
        switch (signDays) {
            case 7:
                rewardPoints = 10;  //连续签到7天，奖励10分
                break;
            case 14:
                rewardPoints = 20;
                break;
            case 28:
                rewardPoints = 40;
                break;
        }

        //9.保存当前用户的积分明细记录到数据库中，后续积分功能要用到；
        rabbitMqHelper.send(
                MqConstants.Exchange.LEARNING_EXCHANGE,
                MqConstants.Key.SIGN_IN,    //签到sign.in
                SignInMessage.of(userId, rewardPoints + 1));//连续签到得分 + 签到基本分

        //10.返回VO；
        SignResultVO signResultVO = new SignResultVO();
        //这里不用设置签到得分，成员属性给了初始值1
        signResultVO.setSignDays(signDays);//连续签到天数
        signResultVO.setRewardPoints(rewardPoints); //连续签到奖励积分
        return signResultVO;
    }


    /**
     * 查询本月签到记录
     *
     * @return 当月每一天的签到记录
     */
    @Override
    public Byte[] querySignRecords() {
        log.info("查询签到记录");

        //1.获取当前登录用户id；
        Long userId = UserContext.getUser();

        //2.获取当天日期；
        LocalDate now = LocalDate.now();

        //3.组装BitMap的key：前缀：sign:uid:用户id:月份信息
        String key = RedisConstants.SIGN_RECORD_KEY_PREFIX
                + userId
                + now.format(DateUtils.SIGN_DATE_SUFFIX_FORMATTER);  //:20230730

        //4.获取今日的号数，14号则一次性获取14个bit位
        int dayOfMonth = now.getDayOfMonth();

        //5.读取Redis，获取指定范围内的每一天签到情况
        //result：bitField命令的所有执行结果。
        List<Long> result = redisTemplate.opsForValue().bitField(key, BitFieldSubCommands.create()
                //0从容器的第一个开始获取，连续获取dayOfMonth个
                .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0));

        //6.读取结果，为null，则表示没签到记录
        if (result == null) {
            return new Byte[0];
        }

        //获取签到记录，十进制数
        int num = result.get(0).intValue();

        //7.定义数组，存储签到记录
        Byte[] everyday = new Byte[dayOfMonth];

        //8.循环遍历十进制数，获取每一个二进制数bit位
        int offset = dayOfMonth - 1;    //作为容器的索引位置,从最后一个bit位开始遍历获取

        while (offset >= 0) {
            //获取当前索引位置对应的bit位结果，索引位置-1
            everyday[offset--] = (byte) (num & 1);
            //把数字右移一位，抛弃最后一个bit位，继续下一个bit位
            num >>>= 1;
        }
        return everyday;
    }


    /**
     * 计算当前用户本月的连续签到天数
     *
     * @param key BitMap的key
     * @param len 本月的第几天
     * @return 本月的连续签到天数
     */
    private int countSignDays(String key, int len) {
        //1.获取本月从第一天截至到今天的所有签到记录。
        //因为BitField命令，有很多子命令，所以返回的是一个集合。我们只用了一个子命令，所以集合里面只会有一个结果;
        List<Long> result = redisTemplate.opsForValue().bitField(key, BitFieldSubCommands.create()
                //len：获取多少个bit位     valueAt：从容器的第一个开始
                .get(BitFieldSubCommands.BitFieldType.unsigned(len)).valueAt(0));
        if (CollUtils.isEmpty(result)) {
            return 0;
        }

        //签到记录，作为一个十进制数字返回
        int num = result.get(0).intValue();

        // 2.定义一个计数器
        int count = 0;

        //3.循环，与1做与&运算，得到最后一个bit，判断是否为0，为0则终止，为1则继续
        while ((num & 1) == 1) {   //==1：表示这天签到了
            // 4.计数器+1
            count++;
            // 5.把数字右移一位，最后一位变成小数被舍弃，倒数第二位成了最后一位
            num >>>= 1;   //无符号整数右移一位
        }
        return count;
    }
}
